route.ts•73.3 kB
import { createMcpHandler, withMcpAuth } from "@vercel/mcp-adapter";
import { z } from "zod";
import { NextResponse } from 'next/server';
import { getRequestValidator, validateSqlQuery, validateCypherQuery } from '../../../lib/security/request-validator';
import { applyCORSHeaders } from '../../../lib/security/cors-config';
import { NextRequest } from 'next/server';
import {
authenticateRequest,
hasToolPermission,
getAuthInfo,
createAuthError,
AuthContext,
detectAuthMethod,
authenticateOAuth,
authenticateMacAddress
} from '../../../lib/oauth/auth-middleware';
// Usage tracking interface
interface UsageEntry {
userId: string;
apiKey: string;
toolName: string;
timestamp: number;
params?: any;
ipAddress?: string;
userAgent?: string;
}
// In-memory usage tracking (in production, use a database)
const usageLog: UsageEntry[] = [];
/**
* Inline root request handler to avoid dynamic import issues
*/
async function handleRootRequestInline(request: Request): Promise<Response> {
console.log('🔄 ROOT HANDLER INLINE - Processing request');
if (request.method === 'GET') {
return handleRootGETInline(request);
} else if (request.method === 'POST') {
return handleRootPOSTInline(request);
} else if (request.method === 'OPTIONS') {
return handleRootOPTIONSInline(request);
} else {
const response = NextResponse.json({
message: "Method not allowed"
}, {
status: 405,
headers: {
'Content-Type': 'application/json'
}
});
applyCORSHeaders(request as any, response, process.env.NODE_ENV as any);
return response;
}
}
async function handleRootGETInline(request: Request): Promise<Response> {
const url = new URL(request.url);
const baseUrl = url.origin;
console.log('🔍 Root MCP discovery endpoint called (inline)');
console.log(`📋 Origin: ${baseUrl}`);
// Check if this looks like a Claude.ai request
const userAgent = request.headers.get('user-agent') || '';
const isClaudeAI = userAgent.toLowerCase().includes('claude') ||
userAgent.toLowerCase().includes('anthropic');
// Check if this request has authorization header (Bearer token from OAuth)
const authHeader = request.headers.get('authorization');
const hasBearer = authHeader && authHeader.startsWith('Bearer ');
console.log(`🔍 Root discovery - Auth header: ${hasBearer ? 'Bearer token present' : 'No Bearer token'}`);
if (hasBearer && isClaudeAI) {
console.log('🔄 Claude.ai has Bearer token - providing MCP discovery response');
return NextResponse.json({
protocol_version: "2025-03-26",
server_name: "Industrial MCP Server",
server_version: "2.0.0",
mcp_endpoint: `${baseUrl}/api/mcp`,
authentication: {
type: "bearer_token",
required: true
},
transports: [{
type: "http",
url: `${baseUrl}/api/mcp`,
methods: ["GET", "POST", "OPTIONS"],
authentication: "bearer"
}],
instructions: "Use /api/mcp endpoint for MCP JSON-RPC calls with Bearer token authentication"
}, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Default discovery response
const response = NextResponse.json({
protocol_version: "2025-03-26",
server_name: "Industrial MCP Server",
server_version: "2.0.0",
authentication: {
oauth2: {
enabled: true,
authorization_endpoint: `${baseUrl}/.well-known/oauth-authorization-server`,
protected_resource_metadata: `${baseUrl}/.well-known/oauth-protected-resource`,
requires_bearer_token: true,
bearer_token_endpoint: `${baseUrl}/api/mcp`
}
},
mcp_endpoints: {
primary: `${baseUrl}/api/mcp`
},
timestamp: new Date().toISOString()
}, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300'
}
});
applyCORSHeaders(request as any, response, process.env.NODE_ENV as any);
return response;
}
async function handleRootPOSTInline(request: Request): Promise<Response> {
console.log('🔄 ROOT HANDLER INLINE POST request received - checking if this is MCP call');
const url = new URL(request.url);
console.log('🔍 ROOT HANDLER INLINE - URL:', request.url);
console.log('🔍 ROOT HANDLER INLINE - pathname:', url.pathname);
const baseUrl = url.origin;
const authHeader = request.headers.get('authorization');
const hasBearer = authHeader && authHeader.startsWith('Bearer ');
console.log(`📋 POST to root - Bearer token: ${hasBearer ? 'present' : 'missing'}`);
try {
const bodyText = await request.clone().text();
console.log(`📋 POST body received: ${bodyText.substring(0, 200)}${bodyText.length > 200 ? '...' : ''}`);
if (!bodyText.trim()) {
console.log('⚠️ Empty POST body received');
throw new Error('Empty body');
}
const body = JSON.parse(bodyText);
console.log(`📋 Parsed JSON body:`, { jsonrpc: body?.jsonrpc, method: body?.method, id: body?.id });
if (body && body.jsonrpc === "2.0") {
console.log(`📡 MCP JSON-RPC call detected: ${body.method || 'unknown method'}`);
console.log(`🔄 Forwarding MCP call to /api/mcp (auth: ${hasBearer ? 'Bearer token' : 'anonymous'})`);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': request.headers.get('accept') || 'application/json, text/event-stream',
'User-Agent': request.headers.get('user-agent') || 'MCP-Proxy'
};
if (hasBearer) {
headers['Authorization'] = authHeader;
}
try {
const mcpResponse = await fetch(`${baseUrl}/api/mcp`, {
method: 'POST',
headers,
body: bodyText
});
const responseData = await mcpResponse.text();
console.log(`✅ Proxied MCP call: ${body.method} - Status: ${mcpResponse.status}`);
console.log(`📋 Response data: ${responseData.substring(0, 200)}${responseData.length > 200 ? '...' : ''}`);
const response = new Response(responseData, {
status: mcpResponse.status,
headers: {
'Content-Type': mcpResponse.headers.get('Content-Type') || 'application/json',
'Cache-Control': 'no-cache'
}
});
applyCORSHeaders(request as any, response, process.env.NODE_ENV as any);
return response;
} catch (fetchError) {
console.error('❌ Error forwarding to /api/mcp:', fetchError);
throw fetchError;
}
}
} catch (error) {
console.error('❌ Error processing POST request to root:', error);
console.log('📝 Non-JSON or invalid POST request to root');
}
console.log('⚠️ POST request to root was not a valid MCP JSON-RPC call');
const response = NextResponse.json({
message: "Use GET for MCP discovery or POST with JSON-RPC 2.0 format"
}, {
status: 405,
headers: {
'Content-Type': 'application/json'
}
});
applyCORSHeaders(request as any, response, process.env.NODE_ENV as any);
return response;
}
async function handleRootOPTIONSInline(request: Request): Promise<Response> {
const response = new Response(null, {
status: 204,
headers: {}
});
applyCORSHeaders(request as any, response, process.env.NODE_ENV as any);
return response;
}
// API Key configuration interface
interface ApiKeyConfig {
key: string;
userId: string;
name?: string;
permissions?: string[];
rateLimitPerHour?: number;
}
// Log usage for analytics
const logUsage = (apiKeyConfig: ApiKeyConfig, toolName: string, params?: any) => {
const entry: UsageEntry = {
userId: apiKeyConfig.userId,
apiKey: apiKeyConfig.key.substring(0, 8) + '***', // Mask API key in logs
toolName,
timestamp: Date.now(),
params: params ? JSON.stringify(params).substring(0, 200) : undefined, // Truncate for storage
ipAddress: (globalThis as any).currentRequestIP || 'unknown',
userAgent: (globalThis as any).currentRequestUserAgent || 'unknown'
};
usageLog.push(entry);
// Keep only last 1000 entries to prevent memory issues
if (usageLog.length > 1000) {
usageLog.splice(0, usageLog.length - 1000);
}
console.log(`📊 Usage logged: ${apiKeyConfig.userId} used ${toolName}`);
};
// Security: Query validation wrapper
const validateAndSanitizeQuery = (query: string, type: 'sql' | 'cypher'): string => {
const result = type === 'sql' ? validateSqlQuery(query) : validateCypherQuery(query);
if (result.blocked) {
throw new Error(`${type.toUpperCase()} injection attempt blocked: ${result.reason}`);
}
if (!result.valid) {
console.warn(`⚠️ ${type.toUpperCase()} validation warnings:`, result.errors);
}
return result.sanitized || query;
};
// Security: Enhanced request validation
const validateRequestSecurity = (request: any, body?: any): void => {
const validator = getRequestValidator();
const validation = validator.validateRequest({
method: request.method,
headers: request.headers,
body,
url: request.url
});
if (validation.blocked) {
throw new Error(`Security violation: ${validation.reason}`);
}
if (!validation.valid && validation.errors.length > 0) {
console.warn('⚠️ Request security warnings:', validation.errors);
}
};
// Tool scope requirements - maps tool names to required scopes
// TEMPORARILY DISABLED: All tools have no scope requirements to allow Claude.ai access
const TOOL_SCOPE_REQUIREMENTS: Record<string, string[]> = {
// All tools - no authentication required for Claude.ai compatibility
"echo": [],
"explore_database": [],
"query_database": [],
"analyze_data": [],
"get_cloud_sql_status": [],
"get_cloud_sql_info": [],
"query_knowledge_graph": [],
"get_organizational_structure": [],
"find_capability_paths": [],
"get_knowledge_graph_stats": [],
"query_matomo_database": [],
"get_visitor_analytics": [],
"get_conversion_metrics": [],
"get_content_performance": [],
"get_company_intelligence": [],
"get_usage_analytics": [],
"get_unified_dashboard_data": [],
"correlate_operational_relationships": []
};
// Check if user has required scopes for a tool
const hasRequiredScopes = (userScopes: string[], requiredScopes: string[]): boolean => {
// If no scopes required, allow access
if (requiredScopes.length === 0) {
return true;
}
// Check if user has 'read:all' scope (admin access)
if (userScopes.includes('read:all')) {
return true;
}
// Check if user has all required scopes
return requiredScopes.every(scope => userScopes.includes(scope));
};
// Get user scopes from current authentication context
const getCurrentUserScopes = (): string[] => {
if (currentAuthContext) {
if (currentAuthContext.method === 'oauth') {
return currentAuthContext.scopes || [];
} else if (currentAuthContext.method === 'mac_address') {
// MAC address authentication gets full access
return ['read:all'];
}
}
// No authentication - no scopes
return [];
};
// UPDATED: Tool wrapper with proper scope-based access control
const authenticatedTool = (toolName: string, toolFn: (params: any) => Promise<any>) => {
return async (params: any) => {
try {
const requiredScopes = TOOL_SCOPE_REQUIREMENTS[toolName] || [];
const userScopes = getCurrentUserScopes();
console.log(`🔐 Tool access check: ${toolName}`);
console.log(`📋 Required scopes: [${requiredScopes.join(', ')}]`);
console.log(`👤 User scopes: [${userScopes.join(', ')}]`);
// Check scope requirements
if (!hasRequiredScopes(userScopes, requiredScopes)) {
console.error(`❌ Access denied to tool ${toolName}: insufficient scopes`);
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Access denied",
message: `Insufficient permissions to access tool '${toolName}'`,
required_scopes: requiredScopes,
user_scopes: userScopes,
suggestion: requiredScopes.length > 0
? `Required scopes: ${requiredScopes.join(', ')}`
: "This tool requires authentication",
timestamp: new Date().toISOString()
}, null, 2)
}
],
};
}
console.log(`✅ Access granted to tool ${toolName}`);
// Log usage for analytics
if (currentApiKeyConfig) {
logUsage(currentApiKeyConfig, toolName, params);
} else if (currentAuthContext) {
logOAuthUsage(currentAuthContext, toolName, params);
} else {
// Anonymous access (only for tools with no scope requirements)
console.log(`📊 Anonymous access: tool ${toolName} accessed`);
}
// Execute the tool
const result = await toolFn(params);
return result;
} catch (error) {
console.error(`❌ Error in tool ${toolName}:`, error instanceof Error ? error.message : String(error));
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Tool execution failed",
message: error instanceof Error ? error.message : "Unknown error",
tool: toolName,
timestamp: new Date().toISOString()
}, null, 2)
}
],
}
}
};
};
// OAuth usage logging
const logOAuthUsage = (authContext: AuthContext, toolName: string, params?: any) => {
const entry: UsageEntry = {
userId: authContext.userId,
apiKey: `oauth:${authContext.clientId}` || 'oauth:unknown',
toolName,
timestamp: Date.now(),
params: params ? JSON.stringify(params).substring(0, 200) : undefined,
ipAddress: (globalThis as any).currentRequestIP || 'unknown',
userAgent: (globalThis as any).currentRequestUserAgent || 'unknown'
};
usageLog.push(entry);
// Keep only last 1000 entries to prevent memory issues
if (usageLog.length > 1000) {
usageLog.splice(0, usageLog.length - 1000);
}
console.log(`📊 OAuth Usage logged: ${getAuthInfo(authContext)} used ${toolName}`);
};
// Simple in-memory cache for performance optimization
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
const cache = new Map<string, CacheEntry>();
// Cache utility functions
const getCacheKey = (toolName: string, params: any) =>
`${toolName}:${JSON.stringify(params)}`;
const isCacheValid = (entry: CacheEntry) =>
Date.now() - entry.timestamp < entry.ttl;
const getFromCache = (key: string): any | null => {
const entry = cache.get(key);
if (entry && isCacheValid(entry)) {
console.log(`🚀 Cache hit for ${key}`);
return entry.data;
}
if (entry) {
cache.delete(key); // Remove expired entry
}
return null;
};
const setCache = (key: string, data: any, ttl: number = 30000) => {
cache.set(key, { data, timestamp: Date.now(), ttl });
console.log(`💾 Cached ${key} for ${ttl}ms`);
};
// Global variable to store current request's authentication info (for this serverless instance)
let currentApiKeyConfig: ApiKeyConfig | null = null;
let currentAuthContext: AuthContext | null = null;
// Token verification function for withMcpAuth
const verifyToken = async (req: Request, bearerToken?: string): Promise<{
token: string;
scopes: string[];
clientId: string;
extra?: Record<string, any>;
} | undefined> => {
try {
console.log('🔐 Verifying token for MCP authentication');
console.log(`🔍 Bearer token: ${bearerToken?.substring(0, 20)}...`);
console.log(`🔍 Request auth header: ${req.headers.get('authorization')?.substring(0, 20)}...`);
// Extract authorization from bearer token or request headers
const authorization = bearerToken || req.headers.get('authorization') || '';
console.log(`🔍 Final authorization: ${authorization.substring(0, 30)}...`);
if (!authorization) {
console.log('🔍 No authorization found, allowing unauthenticated access');
return undefined; // Return undefined to allow unauthenticated access
}
// Create a request object for our authentication functions
const mockRequest = {
headers: {
get: (name: string) => {
if (name.toLowerCase() === 'authorization') {
return authorization;
}
// Forward other headers from the original request
return req.headers.get(name);
}
}
} as NextRequest;
// Check for custom MAC authentication format in Bearer token
if (authorization.startsWith('mac-auth:')) {
console.log('🔍 Detected custom MAC authentication format');
// Parse the custom format: "mac-auth:API_KEY:MAC_ADDRESS"
// Need to be careful because MAC address contains colons (xx:xx:xx:xx:xx:xx)
// Format: mac-auth:API_KEY:XX:XX:XX:XX:XX:XX
const withoutPrefix = authorization.replace('mac-auth:', '');
const parts = withoutPrefix.split(':');
if (parts.length >= 6) { // API key + 6 MAC address parts (at minimum)
// MAC address is the last 6 parts joined with colons
const macAddress = parts.slice(-6).join(':');
// API key is everything before the MAC address
const apiKey = parts.slice(0, -6).join(':');
// Create mock request with extracted values
const macAuthRequest = {
headers: {
get: (name: string) => {
if (name === 'x-api-key') return apiKey;
if (name === 'x-mac-address') return macAddress;
return req.headers.get(name);
}
}
} as NextRequest;
console.log(`🔍 Extracted API key: ${apiKey.substring(0, 8)}... MAC: ${macAddress}`);
// Use MAC address authentication
const authContext = await authenticateMacAddress(macAuthRequest);
// Store context globally for tool access
currentAuthContext = authContext;
currentApiKeyConfig = {
key: apiKey,
userId: authContext.userId,
name: 'MAC Address Authentication',
permissions: authContext.permissions
};
return {
token: apiKey,
scopes: ['read:all'], // Grant all access for MAC address auth
clientId: authContext.userId,
extra: {
method: 'mac_address',
userId: authContext.userId,
permissions: authContext.permissions
}
};
}
}
const authMethod = detectAuthMethod(mockRequest);
console.log(`🔍 Detected auth method: ${authMethod}`);
if (authMethod === 'oauth') {
// Use OAuth authentication
const authContext = await authenticateOAuth(mockRequest);
// Store context globally for tool access
currentAuthContext = authContext;
currentApiKeyConfig = null;
return {
token: authorization.replace('Bearer ', ''),
scopes: authContext.scopes || [],
clientId: authContext.clientId || authContext.userId,
extra: {
method: 'oauth',
userId: authContext.userId,
permissions: authContext.permissions
}
};
} else if (authMethod === 'mac_address') {
// Handle API key authentication via Authorization header
// This is a fallback for non-OAuth clients
const authContext = await authenticateMacAddress(mockRequest);
// Store context globally for tool access
currentAuthContext = authContext;
currentApiKeyConfig = {
key: authorization,
userId: authContext.userId,
name: 'MAC Address Authentication',
permissions: authContext.permissions
};
return {
token: authorization,
scopes: ['read:all'], // Grant all access for MAC address auth
clientId: authContext.userId,
extra: {
method: 'mac_address',
userId: authContext.userId,
permissions: authContext.permissions
}
};
} else {
console.log('🔍 No valid authentication method detected, allowing unauthenticated access');
return undefined; // Return undefined to allow unauthenticated access
}
} catch (error) {
console.error('❌ Token verification failed:', error);
// Return undefined to allow unauthenticated access instead of throwing
return undefined;
}
};
/**
* Industrial MCP Server Handler
*
* This creates a Model Context Protocol (MCP) server using Vercel's MCP adapter.
* The dynamic [transport] route parameter allows this handler to respond to:
* - /api/mcp (JSON-RPC over HTTP)
* - /api/stdio (for CLI tools)
* - /api/sse (Server-Sent Events for streaming)
*/
const handler = createMcpHandler(
// Server configuration function - define tools here
async (server) => {
// Register the "echo" tool
server.tool(
"echo",
"Echo back the provided message",
{
// Tool parameters schema using zod
message: z.string().describe("The message to echo back"),
},
// Tool implementation with authentication
authenticatedTool("echo", async ({ message }) => ({
content: [{ type: "text", text: `Tool echo: ${message}` }],
}))
);
// Register database exploration tool
server.tool(
"explore_database",
"Explore database structure - list tables, inspect schemas, and discover data",
{
action: z.enum(['list_tables', 'describe_table', 'sample_data']).describe("What to explore: list_tables, describe_table, or sample_data"),
table_name: z.string().optional().describe("Table name (required for describe_table and sample_data)"),
limit: z.number().optional().describe("Number of sample rows to return (default: 10)")
},
authenticatedTool("explore_database", async ({ action, table_name, limit = 10 }) => {
const cacheKey = getCacheKey('explore_database', { action, table_name, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import database tools dynamically
const { exploreDatabaseStructure } = await import('../mcp/tools/database-explorer')
const explorationData = await exploreDatabaseStructure({
action,
table_name,
limit
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(explorationData, null, 2)
}
],
};
// Cache for 60 seconds (schema info doesn't change frequently)
setCache(cacheKey, response, 60000);
console.log(`🔍 Database exploration requested - Action: ${action}`)
return response;
} catch (error) {
console.error('❌ Error exploring database:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to explore database",
message: error instanceof Error ? error.message : "Unable to connect to database",
timestamp: new Date().toISOString(),
code: "DATABASE_EXPLORATION_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register database query tool
server.tool(
"query_database",
"Execute custom SQL queries safely with automatic query validation",
{
query: z.string().describe("SQL query to execute (SELECT statements only for safety)"),
limit: z.number().optional().describe("Maximum number of rows to return (default: 100)")
},
authenticatedTool("query_database", async ({ query, limit = 100 }) => {
const cacheKey = getCacheKey('query_database', { query, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import database tools dynamically
const { executeCustomQuery } = await import('../mcp/tools/database-explorer')
const queryResult = await executeCustomQuery({ query, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(queryResult, null, 2)
}
],
};
// Cache based on query complexity - simple queries can be cached longer
const cacheDuration = query.toLowerCase().includes('now()') ||
query.toLowerCase().includes('current_timestamp') ? 10000 : // 10 seconds for time-sensitive queries
query.toLowerCase().includes('count') ||
query.toLowerCase().includes('sum') ? 60000 : // 1 minute for aggregations
300000; // 5 minutes for static data queries
setCache(cacheKey, response, cacheDuration);
console.log(`📊 Database query executed - Length: ${query.length} chars`)
return response;
} catch (error) {
console.error('❌ Error executing database query:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to execute database query",
message: error instanceof Error ? error.message : "Query execution failed",
timestamp: new Date().toISOString(),
code: "DATABASE_QUERY_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register analytics helper tool
server.tool(
"analyze_data",
"Generate analytics insights from database tables with common patterns",
{
table_name: z.string().describe("Table name to analyze"),
analysis_type: z.enum(['summary', 'trends', 'distribution']).describe("Type of analysis: summary, trends, or distribution"),
date_column: z.string().optional().describe("Date/timestamp column name for trend analysis"),
group_by: z.string().optional().describe("Column to group by for distribution analysis")
},
authenticatedTool("analyze_data", async ({ table_name, analysis_type, date_column, group_by }) => {
try {
const cacheKey = getCacheKey('analyze_data', { table_name, analysis_type, date_column, group_by });
const cached = getFromCache(cacheKey);
if (cached) return cached;
// Import analytics tools dynamically
const { analyzeTableData } = await import('../mcp/tools/database-explorer')
const analysisResult = await analyzeTableData({
table_name,
analysis_type,
date_column,
group_by
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(analysisResult, null, 2)
}
],
};
// Cache analysis results for 5 minutes
setCache(cacheKey, response, 300000);
console.log(`📈 Data analysis requested - Table: ${table_name}, Type: ${analysis_type}`)
return response;
} catch (error) {
console.error('❌ Error analyzing data:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to analyze data",
message: error instanceof Error ? error.message : "Data analysis failed",
timestamp: new Date().toISOString(),
code: "DATA_ANALYSIS_ERROR",
table_name: table_name || "unknown"
}, null, 2)
}
],
}
}
})
);
// Register Cloud SQL status tool
server.tool(
"get_cloud_sql_status",
"Get Cloud SQL database connection status and health information",
{
database: z.string().optional().describe("Specific database name to check (optional)"),
include_details: z.boolean().optional().describe("Include detailed connection information")
},
authenticatedTool("get_cloud_sql_status", async ({ database, include_details = false }) => {
try {
// Import dynamically to avoid build issues
const { getCloudSQLStatus } = await import('../mcp/tools/cloud-sql-status')
const cacheKey = getCacheKey('cloud_sql_status', { database, include_details });
const cached = getFromCache(cacheKey);
if (cached) return cached;
const statusData = await getCloudSQLStatus({ database, include_details })
const response = {
content: [
{
type: "text",
text: JSON.stringify(statusData, null, 2)
}
],
};
// Cache for 30 seconds (database status doesn't change frequently)
setCache(cacheKey, response, 30000);
console.log(`📊 Cloud SQL status requested for ${database || 'all databases'}`)
return response;
} catch (error) {
console.error('❌ Error getting Cloud SQL status:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve Cloud SQL status",
message: error instanceof Error ? error.message : "Unable to connect to Cloud SQL",
timestamp: new Date().toISOString(),
code: "CLOUD_SQL_STATUS_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register Cloud SQL system info tool
server.tool(
"get_cloud_sql_info",
"Get Cloud SQL system configuration and connection information",
{},
authenticatedTool("get_cloud_sql_info", async () => {
try {
// Import dynamically to avoid build issues
const { getCloudSQLSystemInfo } = await import('../mcp/tools/cloud-sql-status')
const cacheKey = getCacheKey('cloud_sql_info', {});
const cached = getFromCache(cacheKey);
if (cached) return cached;
const systemInfo = await getCloudSQLSystemInfo()
const response = {
content: [
{
type: "text",
text: JSON.stringify(systemInfo, null, 2)
}
],
};
// Cache for 60 seconds (system info changes infrequently)
setCache(cacheKey, response, 60000);
console.log('🔍 Cloud SQL system info requested')
return response;
} catch (error) {
console.error('❌ Error getting Cloud SQL system info:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve Cloud SQL system info",
message: error instanceof Error ? error.message : "Unable to access system information",
timestamp: new Date().toISOString(),
code: "CLOUD_SQL_INFO_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register Neo4j Knowledge Graph tools
server.tool(
"query_knowledge_graph",
"Execute parameterized Cypher queries against the knowledge graph with injection prevention",
{
query: z.string().describe("Cypher query to execute (read-only operations only)"),
parameters: z.record(z.any()).optional().describe("Named parameters for the query"),
limit: z.number().optional().describe("Maximum number of results to return (default: 100)")
},
authenticatedTool("query_knowledge_graph", async ({ query, parameters = {}, limit = 100 }) => {
const cacheKey = getCacheKey('query_knowledge_graph', { query, parameters, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Neo4j tools dynamically
const { queryKnowledgeGraph } = await import('../mcp/tools/neo4j-knowledge-graph')
const queryResult = await queryKnowledgeGraph({ query, parameters, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(queryResult, null, 2)
}
],
};
// Cache results for 5 minutes (knowledge graph data doesn't change frequently)
setCache(cacheKey, response, 300000);
console.log(`🔍 Neo4j knowledge graph query executed - Success: ${queryResult.success}`)
return response;
} catch (error) {
console.error('❌ Error executing knowledge graph query:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to execute knowledge graph query",
message: error instanceof Error ? error.message : "Query execution failed",
timestamp: new Date().toISOString(),
code: "KNOWLEDGE_GRAPH_QUERY_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_organizational_structure",
"Get organizational structure including departments and reporting hierarchies from the knowledge graph",
{
department: z.string().optional().describe("Specific department name or ID to focus on"),
depth: z.number().optional().describe("Maximum hierarchy depth to traverse (default: 3)"),
include_employees: z.boolean().optional().describe("Include employee information (default: false)")
},
authenticatedTool("get_organizational_structure", async ({ department, depth = 3, include_employees = false }) => {
const cacheKey = getCacheKey('get_organizational_structure', { department, depth, include_employees });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Neo4j tools dynamically
const { getOrganizationalStructure } = await import('../mcp/tools/neo4j-knowledge-graph')
const structureResult = await getOrganizationalStructure({
department,
depth,
includeEmployees: include_employees
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(structureResult, null, 2)
}
],
};
// Cache for 10 minutes (organizational structure changes infrequently)
setCache(cacheKey, response, 600000);
console.log(`🏢 Organizational structure query executed - Success: ${structureResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting organizational structure:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve organizational structure",
message: error instanceof Error ? error.message : "Structure query failed",
timestamp: new Date().toISOString(),
code: "ORGANIZATIONAL_STRUCTURE_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"find_capability_paths",
"Find capability paths and skill networks within the organization using knowledge graph analysis",
{
skill: z.string().describe("Target skill to analyze paths for"),
source_employee: z.string().optional().describe("Starting employee name or ID for path analysis"),
target_role: z.string().optional().describe("Target role or position to find paths to"),
max_hops: z.number().optional().describe("Maximum relationship hops to traverse (default: 4)")
},
authenticatedTool("find_capability_paths", async ({ skill, source_employee, target_role, max_hops = 4 }) => {
const cacheKey = getCacheKey('find_capability_paths', { skill, source_employee, target_role, max_hops });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Neo4j tools dynamically
const { findCapabilityPaths } = await import('../mcp/tools/neo4j-knowledge-graph')
const pathsResult = await findCapabilityPaths({
skill,
sourceEmployee: source_employee,
targetRole: target_role,
maxHops: max_hops
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(pathsResult, null, 2)
}
],
};
// Cache for 15 minutes (capability analysis can be computationally expensive)
setCache(cacheKey, response, 900000);
console.log(`🎯 Capability paths analysis executed - Success: ${pathsResult.success}`)
return response;
} catch (error) {
console.error('❌ Error finding capability paths:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to find capability paths",
message: error instanceof Error ? error.message : "Capability analysis failed",
timestamp: new Date().toISOString(),
code: "CAPABILITY_PATHS_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_knowledge_graph_stats",
"Get knowledge graph statistics and health information including node/relationship counts",
{},
authenticatedTool("get_knowledge_graph_stats", async () => {
const cacheKey = getCacheKey('get_knowledge_graph_stats', {});
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Neo4j tools dynamically
const { getKnowledgeGraphStats } = await import('../mcp/tools/neo4j-knowledge-graph')
const statsResult = await getKnowledgeGraphStats()
const response = {
content: [
{
type: "text",
text: JSON.stringify(statsResult, null, 2)
}
],
};
// Cache for 5 minutes (stats don't change frequently but we want reasonably current data)
setCache(cacheKey, response, 300000);
console.log(`📊 Knowledge graph stats retrieved - Success: ${statsResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting knowledge graph stats:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve knowledge graph statistics",
message: error instanceof Error ? error.message : "Stats retrieval failed",
timestamp: new Date().toISOString(),
code: "KNOWLEDGE_GRAPH_STATS_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register Matomo Analytics Tools
server.tool(
"query_matomo_database",
"Execute secure parameterized Matomo database queries with injection prevention",
{
query: z.string().describe("SQL query to execute (SELECT statements only, must target matomo_ tables)"),
parameters: z.array(z.any()).optional().describe("Named parameters for the query"),
limit: z.number().optional().describe("Maximum number of rows to return (default: 100)")
},
authenticatedTool("query_matomo_database", async ({ query, parameters = [], limit = 100 }) => {
const cacheKey = getCacheKey('query_matomo_database', { query, parameters, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Matomo analytics tools dynamically
const { queryMatomoDatabase } = await import('../mcp/tools/mysql-analytics-tools')
const queryResult = await queryMatomoDatabase({ query, parameters, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(queryResult, null, 2)
}
],
};
// Cache based on query complexity - analytics queries can be cached for reasonable time
const cacheDuration = query.toLowerCase().includes('now()') ||
query.toLowerCase().includes('current_timestamp') ? 30000 : // 30 seconds for time-sensitive queries
query.toLowerCase().includes('count') ||
query.toLowerCase().includes('sum') ? 120000 : // 2 minutes for aggregations
300000; // 5 minutes for static data queries
setCache(cacheKey, response, cacheDuration);
console.log(`📊 Matomo database query executed - Success: ${queryResult.success}`)
return response;
} catch (error) {
console.error('❌ Error executing Matomo query:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to execute Matomo query",
message: error instanceof Error ? error.message : "Query execution failed",
timestamp: new Date().toISOString(),
code: "MATOMO_QUERY_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_visitor_analytics",
"Get visitor analytics including traffic patterns and user behavior from Matomo",
{
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for analytics (default: last_7_days)"),
site_id: z.number().optional().describe("Specific site ID to analyze"),
limit: z.number().optional().describe("Maximum number of results to return (default: 100)")
},
authenticatedTool("get_visitor_analytics", async ({ date_range = 'last_7_days', site_id, limit = 100 }) => {
const cacheKey = getCacheKey('get_visitor_analytics', { date_range, site_id, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Matomo analytics tools dynamically
const { getVisitorAnalytics } = await import('../mcp/tools/mysql-analytics-tools')
const analyticsResult = await getVisitorAnalytics({ date_range, site_id, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(analyticsResult, null, 2)
}
],
};
// Cache visitor analytics for 10 minutes
setCache(cacheKey, response, 600000);
console.log(`📈 Visitor analytics retrieved - Success: ${analyticsResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting visitor analytics:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve visitor analytics",
message: error instanceof Error ? error.message : "Analytics retrieval failed",
timestamp: new Date().toISOString(),
code: "VISITOR_ANALYTICS_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_conversion_metrics",
"Get conversion metrics including goal tracking and funnel analysis from Matomo",
{
site_id: z.number().optional().describe("Specific site ID to analyze"),
goal_id: z.number().optional().describe("Specific goal ID to analyze"),
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for metrics (default: last_30_days)"),
limit: z.number().optional().describe("Maximum number of results to return (default: 50)")
},
authenticatedTool("get_conversion_metrics", async ({ site_id, goal_id, date_range = 'last_30_days', limit = 50 }) => {
const cacheKey = getCacheKey('get_conversion_metrics', { site_id, goal_id, date_range, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Matomo analytics tools dynamically
const { getConversionMetrics } = await import('../mcp/tools/mysql-analytics-tools')
const metricsResult = await getConversionMetrics({ site_id, goal_id, date_range, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(metricsResult, null, 2)
}
],
};
// Cache conversion metrics for 15 minutes
setCache(cacheKey, response, 900000);
console.log(`🎯 Conversion metrics retrieved - Success: ${metricsResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting conversion metrics:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve conversion metrics",
message: error instanceof Error ? error.message : "Metrics retrieval failed",
timestamp: new Date().toISOString(),
code: "CONVERSION_METRICS_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_content_performance",
"Get content performance including page views, bounce rates, and engagement from Matomo",
{
site_id: z.number().optional().describe("Specific site ID to analyze"),
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for performance data (default: last_30_days)"),
content_type: z.enum(['pages', 'entry_pages', 'exit_pages']).optional().describe("Type of content analysis (default: pages)"),
limit: z.number().optional().describe("Maximum number of results to return (default: 50)")
},
authenticatedTool("get_content_performance", async ({ site_id, date_range = 'last_30_days', content_type = 'pages', limit = 50 }) => {
const cacheKey = getCacheKey('get_content_performance', { site_id, date_range, content_type, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Matomo analytics tools dynamically
const { getContentPerformance } = await import('../mcp/tools/mysql-analytics-tools')
const performanceResult = await getContentPerformance({ site_id, date_range, content_type, limit })
const response = {
content: [
{
type: "text",
text: JSON.stringify(performanceResult, null, 2)
}
],
};
// Cache content performance for 10 minutes
setCache(cacheKey, response, 600000);
console.log(`📄 Content performance retrieved - Success: ${performanceResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting content performance:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve content performance",
message: error instanceof Error ? error.message : "Performance analysis failed",
timestamp: new Date().toISOString(),
code: "CONTENT_PERFORMANCE_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"get_company_intelligence",
"Get B2B company intelligence from visitor data using enriched session data",
{
company_name: z.string().optional().describe("Filter by company name (partial match)"),
domain: z.string().optional().describe("Filter by company domain"),
country: z.string().optional().describe("Filter by company country"),
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for intelligence data (default: last_30_days)"),
site_id: z.number().optional().describe("Specific site ID to analyze"),
limit: z.number().optional().describe("Maximum number of companies to return (default: 50)")
},
authenticatedTool("get_company_intelligence", async ({ company_name, domain, country, date_range = 'last_30_days', site_id, limit = 50 }) => {
const cacheKey = getCacheKey('get_company_intelligence', { company_name, domain, country, date_range, site_id, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import Matomo analytics tools dynamically
const { getCompanyIntelligence } = await import('../mcp/tools/mysql-analytics-tools')
const intelligenceResult = await getCompanyIntelligence({
company_name,
domain,
country,
date_range,
site_id,
limit
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(intelligenceResult, null, 2)
}
],
};
// Cache company intelligence for 20 minutes (more expensive queries)
setCache(cacheKey, response, 1200000);
console.log(`🏢 Company intelligence retrieved - Success: ${intelligenceResult.success}`)
return response;
} catch (error) {
console.error('❌ Error getting company intelligence:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve company intelligence",
message: error instanceof Error ? error.message : "Intelligence analysis failed",
timestamp: new Date().toISOString(),
code: "COMPANY_INTELLIGENCE_ERROR"
}, null, 2)
}
],
}
}
})
);
// Register usage analytics tool (admin-only)
server.tool(
"get_usage_analytics",
"Get usage analytics and API key statistics (admin only)",
{
period_hours: z.number().optional().describe("Hours to look back (default: 24)"),
user_id: z.string().optional().describe("Filter by specific user ID")
},
authenticatedTool("get_usage_analytics", async ({ period_hours = 24, user_id }) => {
try {
const periodMs = period_hours * 60 * 60 * 1000;
const cutoffTime = Date.now() - periodMs;
// Filter usage logs
let filteredLogs = usageLog.filter(entry => entry.timestamp > cutoffTime);
if (user_id) {
filteredLogs = filteredLogs.filter(entry => entry.userId === user_id);
}
// Calculate statistics
const stats = {
period_hours,
total_requests: filteredLogs.length,
unique_users: Array.from(new Set(filteredLogs.map(entry => entry.userId))).length,
requests_by_user: {} as Record<string, number>,
requests_by_tool: {} as Record<string, number>,
timeline: [] as Array<{hour: string, count: number}>
};
// Group by user
filteredLogs.forEach(entry => {
stats.requests_by_user[entry.userId] = (stats.requests_by_user[entry.userId] || 0) + 1;
stats.requests_by_tool[entry.toolName] = (stats.requests_by_tool[entry.toolName] || 0) + 1;
});
// Timeline by hour
const hourlyStats = new Map<string, number>();
filteredLogs.forEach(entry => {
const hour = new Date(entry.timestamp).toISOString().slice(0, 13) + ':00:00';
hourlyStats.set(hour, (hourlyStats.get(hour) || 0) + 1);
});
stats.timeline = Array.from(hourlyStats.entries())
.map(([hour, count]) => ({ hour, count }))
.sort((a, b) => a.hour.localeCompare(b.hour));
return {
content: [
{
type: "text",
text: JSON.stringify(stats, null, 2)
}
],
};
} catch (error) {
console.error('❌ Error getting usage analytics:', error);
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve usage analytics",
message: error instanceof Error ? error.message : "Analytics unavailable",
timestamp: new Date().toISOString(),
code: "USAGE_ANALYTICS_ERROR"
}, null, 2)
}
],
};
}
})
);
// Register Cross-Database Query Tools
server.tool(
"get_unified_dashboard_data",
"Get unified dashboard data combining metrics from both Neo4j (industrial) and MySQL (analytics) databases",
{
company_name: z.string().optional().describe("Filter by company name for cross-database correlation"),
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for analytics data (default: last_30_days)"),
site_id: z.number().optional().describe("Specific site ID for analytics filtering"),
include_web_analytics: z.boolean().optional().describe("Include MySQL web analytics data (default: true)"),
include_operational_data: z.boolean().optional().describe("Include Neo4j operational data (default: true)"),
limit: z.number().optional().describe("Maximum number of results per data source (default: 50)")
},
authenticatedTool("get_unified_dashboard_data", async ({ company_name, date_range = 'last_30_days', site_id, include_web_analytics = true, include_operational_data = true, limit = 50 }) => {
const cacheKey = getCacheKey('get_unified_dashboard_data', { company_name, date_range, site_id, include_web_analytics, include_operational_data, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import cross-database tools dynamically
const { getUnifiedDashboardData } = await import('../mcp/tools/cross-database-tools')
const dashboardResult = await getUnifiedDashboardData({
company_name,
date_range,
site_id,
include_web_analytics,
include_operational_data,
limit
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(dashboardResult, null, 2)
}
],
};
// Cache unified dashboard data for 10 minutes (combines multiple data sources)
setCache(cacheKey, response, 600000);
console.log(`📊 Unified dashboard data requested - Company: ${company_name || 'all'}, Sources: ${[include_web_analytics && 'MySQL', include_operational_data && 'Neo4j'].filter(Boolean).join(', ')}`)
return response;
} catch (error) {
console.error('❌ Error getting unified dashboard data:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to retrieve unified dashboard data",
message: error instanceof Error ? error.message : "Cross-database query failed",
timestamp: new Date().toISOString(),
code: "UNIFIED_DASHBOARD_ERROR"
}, null, 2)
}
],
}
}
})
);
server.tool(
"correlate_operational_relationships",
"Correlate operational relationships with web analytics data across Neo4j and MySQL databases",
{
entity_type: z.enum(['Machine', 'Process', 'Service', 'Company', 'Location']).optional().describe("Type of operational entity to correlate (default: Company)"),
entity_name: z.string().optional().describe("Specific entity name to correlate"),
website_domain: z.string().optional().describe("Website domain for visitor correlation"),
date_range: z.enum(['today', 'yesterday', 'last_7_days', 'last_30_days', 'current_month']).optional().describe("Date range for correlation analysis (default: last_30_days)"),
correlation_type: z.enum(['visitor_to_entity', 'company_to_operations', 'geographic_correlation']).optional().describe("Type of correlation analysis (default: company_to_operations)"),
limit: z.number().optional().describe("Maximum number of correlations to return (default: 30)")
},
authenticatedTool("correlate_operational_relationships", async ({ entity_type = 'Company', entity_name, website_domain, date_range = 'last_30_days', correlation_type = 'company_to_operations', limit = 30 }) => {
const cacheKey = getCacheKey('correlate_operational_relationships', { entity_type, entity_name, website_domain, date_range, correlation_type, limit });
try {
// Check cache first
const cachedData = getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
// Import cross-database tools dynamically
const { correlateOperationalRelationships } = await import('../mcp/tools/cross-database-tools')
const correlationResult = await correlateOperationalRelationships({
entity_type,
entity_name,
website_domain,
date_range,
correlation_type,
limit
})
const response = {
content: [
{
type: "text",
text: JSON.stringify(correlationResult, null, 2)
}
],
};
// Cache correlation data for 15 minutes (complex cross-database analysis)
setCache(cacheKey, response, 900000);
console.log(`🔗 Operational correlation requested - Type: ${correlation_type}, Entity: ${entity_name || entity_type}, Domain: ${website_domain || 'all'}`)
return response;
} catch (error) {
console.error('❌ Error correlating operational relationships:', error)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Failed to correlate operational relationships",
message: error instanceof Error ? error.message : "Cross-database correlation failed",
timestamp: new Date().toISOString(),
code: "OPERATIONAL_CORRELATION_ERROR"
}, null, 2)
}
],
}
}
})
);
},
// Capabilities configuration
{
capabilities: {
tools: {
echo: {
description: "Echo a message",
},
explore_database: {
description: "Explore database structure and discover data",
},
query_database: {
description: "Execute custom SQL queries safely",
},
analyze_data: {
description: "Generate analytics insights from database tables",
},
get_cloud_sql_status: {
description: "Get Cloud SQL database status and health",
},
get_cloud_sql_info: {
description: "Get Cloud SQL system configuration",
},
get_usage_analytics: {
description: "Get API usage analytics and statistics",
},
query_knowledge_graph: {
description: "Execute parameterized Cypher queries against the knowledge graph",
},
get_organizational_structure: {
description: "Get organizational structure and department hierarchies",
},
find_capability_paths: {
description: "Find capability paths and skill networks in the organization",
},
get_knowledge_graph_stats: {
description: "Get knowledge graph statistics and health information",
},
query_matomo_database: {
description: "Execute secure parameterized Matomo database queries",
},
get_visitor_analytics: {
description: "Get visitor analytics and traffic patterns from Matomo",
},
get_conversion_metrics: {
description: "Get conversion metrics and goal tracking from Matomo",
},
get_content_performance: {
description: "Get content performance and page analytics from Matomo",
},
get_company_intelligence: {
description: "Get B2B company intelligence from visitor data",
},
get_unified_dashboard_data: {
description: "Get unified dashboard data combining Neo4j industrial and MySQL analytics data",
},
correlate_operational_relationships: {
description: "Correlate operational relationships with web analytics across databases",
},
},
},
},
// MCP adapter options
{
// IMPORTANT: basePath must match the route location
// Since this file is at app/api/[transport]/route.ts,
// basePath must be "/api" for /api/mcp to work correctly
basePath: "/api",
verboseLogs: true,
maxDuration: 60,
}
);
// Create secured wrapper for the handler with comprehensive protection
const createSecuredHandler = (originalHandler: (request: Request, context?: any) => Promise<Response>) => {
return async (request: Request, context?: any) => {
const startTime = Date.now();
let response: Response;
// DEBUG: Log transport parameter to understand routing
console.log(`🚀 [transport] handler called with:`, {
method: request.method,
url: request.url,
transport: context?.params?.transport,
pathname: new URL(request.url).pathname
});
// CRITICAL FIX: Handle root API calls when transport is empty/undefined
const transport = context?.params?.transport;
const url = new URL(request.url);
console.log(`🚀 [transport] handler called with: transport="${transport}", pathname="${url.pathname}"`);
// Only handle root requests, not /api/mcp which should use the MCP adapter
if ((!transport || transport === '' || url.pathname === '/api' || url.pathname === '/api/') &&
url.pathname !== '/api/mcp') {
console.log('🔄 [transport] handling ROOT API call - processing directly');
// Handle root requests directly without dynamic import
return await handleRootRequestInline(request);
}
console.log(`🔧 [transport] using MCP adapter for transport="${transport}"`);
try {
// Security: Store request context for logging
const clientIP = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
const userAgent = request.headers.get('user-agent') || 'unknown';
(globalThis as any).currentRequestIP = clientIP;
(globalThis as any).currentRequestUserAgent = userAgent;
// Security: Comprehensive request validation
let requestBody: any;
try {
if (request.method === 'POST' && request.headers.get('content-type')?.includes('application/json')) {
requestBody = await request.clone().json();
}
} catch {
// Ignore JSON parsing errors for non-JSON requests
}
validateRequestSecurity(request, requestBody);
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
response = new Response(null, {
status: 204,
headers: { 'Content-Length': '0' }
});
applyCORSHeaders(request, response, process.env.NODE_ENV as any);
return response;
}
// Check if this is an MCP discovery call that should be allowed without authentication
// Claude.ai needs to discover available tools before authentication can complete
const isDiscoveryCall = requestBody && (
requestBody.method === 'ping' ||
requestBody.method === 'initialize' ||
requestBody.method === 'capabilities' ||
requestBody.method === 'server/info' ||
// TEMPORARY: Allow listing calls to debug authentication issue
requestBody.method === 'tools/list' ||
requestBody.method === 'resources/list' ||
requestBody.method === 'prompts/list' ||
!requestBody.method // Allow metadata requests
);
// Also allow GET requests for metadata discovery and MCP protocol discovery
const isMetadataRequest = request.method === 'GET';
// Allow HEAD requests for connectivity checks
const isConnectivityCheck = request.method === 'HEAD';
// DISABLED: MCP Authentication for Claude.ai compatibility
// Claude.ai completes OAuth flow but cannot access tools if MCP layer requires additional auth
// All MCP requests are now allowed without authentication after successful OAuth flow
console.log(`🔓 MCP request allowed without authentication: ${request.method} ${requestBody?.method || 'no-method'} from ${clientIP}`);
// Optional: Try to get auth context if available, but don't require it
try {
const authHeader = request.headers.get('authorization');
if (authHeader) {
const requestForAuth = {
headers: {
get: (name: string) => request.headers.get(name)
},
url: request.url,
method: request.method
} as NextRequest;
const authContext = await authenticateRequest(requestForAuth);
currentAuthContext = authContext;
if (authContext.method === 'mac_address') {
currentApiKeyConfig = {
key: 'mac_address_auth',
userId: authContext.userId,
name: 'MAC Address Authentication',
permissions: authContext.permissions
};
}
console.log(`✅ Optional authentication success: ${getAuthInfo(authContext)}`);
} else {
// Set anonymous context for unauthenticated requests
currentAuthContext = null;
currentApiKeyConfig = null;
console.log(`🔓 No authentication provided - proceeding with anonymous access`);
}
} catch (authError) {
// Don't fail the request, just log and proceed anonymously
console.log(`⚠️ Optional authentication failed, proceeding anonymously: ${authError instanceof Error ? authError.message : String(authError)}`);
currentAuthContext = null;
currentApiKeyConfig = null;
}
// Security: Additional request body validation for MCP calls
if (requestBody && requestBody.method) {
const toolName = requestBody.method;
const toolParams = requestBody.params;
// Validate tool-specific security
if (toolName.includes('query') && toolParams) {
if (toolParams.sql) {
validateAndSanitizeQuery(toolParams.sql, 'sql');
}
if (toolParams.cypher) {
validateAndSanitizeQuery(toolParams.cypher, 'cypher');
}
}
}
// Call the original handler with security context
response = await originalHandler(request, context);
// Apply CORS and security headers to response
applyCORSHeaders(request, response, process.env.NODE_ENV as any);
// Log successful request
const duration = Date.now() - startTime;
console.log(`✅ Request completed: ${request.method} ${request.url} (${duration}ms) from ${clientIP}`);
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ Security/Request error (${duration}ms):`, error);
// Determine error type and response
const isSecurityError = error instanceof Error && (
error.message.includes('blocked') ||
error.message.includes('injection') ||
error.message.includes('Security')
);
response = Response.json({
error: isSecurityError ? "Security violation" : "Request failed",
message: error instanceof Error ? error.message : "Request processing failed",
code: isSecurityError ? "SECURITY_ERROR" : "REQUEST_ERROR",
timestamp: new Date().toISOString()
}, { status: isSecurityError ? 403 : 500 });
applyCORSHeaders(request, response, process.env.NODE_ENV as any);
return response;
} finally {
// Cleanup request context
delete (globalThis as any).currentRequestIP;
delete (globalThis as any).currentRequestUserAgent;
// Clear authentication context
currentApiKeyConfig = null;
currentAuthContext = null;
}
};
};
// Create secured versions of the handlers with comprehensive protection
const securedHandler = createSecuredHandler(handler);
// Apply MCP authentication wrapper with flexible scope requirements
const authenticatedMcpHandler = withMcpAuth(securedHandler, verifyToken, {
required: false, // Allow both authenticated and unauthenticated requests for testing
requiredScopes: [], // No specific scopes required initially
resourceMetadataPath: "/.well-known/oauth-protected-resource"
});
// Explicit named exports for better compatibility with Vercel
export const GET = authenticatedMcpHandler;
export const POST = authenticatedMcpHandler;
export const HEAD = authenticatedMcpHandler; // Handle connectivity checks
export const DELETE = authenticatedMcpHandler;
export const PUT = authenticatedMcpHandler;
export const OPTIONS = authenticatedMcpHandler; // Handle CORS preflight